package mbsl.storage;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import mbsl.extensions.MissingDependencyException;
import mbsl.system.Core;
import mbsl.system.Logger;
import mbsl.system.TextProvider;
import mbsl.util.DependencyChecker;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

/**
 * A very simple interface to Hibernate, which does all the actual work.
 */
public class Database {
	/**
	 * Creates a new Database.
	 * <p>
	 * This will throw MissingDependencyException if:<br>
	 * Hibernate (org.hibernate.Hibernate) is missing, or
	 * the HSQLDB Driver (org.hsqldb.jdbcDriver) is missing.
	 */
	public Database() throws MissingDependencyException {
		if (!DependencyChecker.check("org.hibernate.Hibernate"))
			throw new MissingDependencyException(tp.get("storage.reqHibernate"));
		if (!DependencyChecker.check("org.hsqldb.jdbcDriver")) // Conveniently, this inadvertently also loads the driver
			throw new MissingDependencyException(tp.get("storage.reqDriver"));
		
		mConfiguration = new Configuration().configure("/hibernate.cfg.xml");
		
		mResources = new ArrayList<String>();
	}
	
	/**
	 * Creates a new DatabaseTableManipulator.
	 * <p>
	 * This method will return <i>null</i> if there was an error connecting to the database.
	 * @see DatabaseTableManipulator
	 */
	public DatabaseTableManipulator getTableManipulator() {
		errorIfStarted("storage.manipulateBeforeStart");

		if (mManipulators == null)
			mManipulators = new ArrayList<Database.TableManipulator>();
		
		try {
			if (mConnection == null)
				mConnection = DriverManager.getConnection(mConfiguration.getProperty("connection.url"));
		} catch (SQLException pE) {
			pE.printStackTrace();
			return null;
		}
		
		TableManipulator manipulator = new TableManipulator();
		
		mManipulators.add(manipulator);
		
		return manipulator;
	}
	
	/**
	 * Adds a resource to the Hibernate configuration
	 */
	public void addResource(String pResourceName) {
		errorIfStarted("storage.resourcesBeforeStart");
		
		mResources.add(pResourceName);
	}
	
	/**
	 * Starts Hibernate.
	 * <p>
	 * This will also render DatabaseTableManipulators useless, and it will no longer be possible to
	 * add resources.
	 */
	public void start() {
		errorIfStarted("storage.alreadyStarted");
		
		logger.info("Starting Hibernate");
		// Close the connection used by the manipulators
		if (mConnection != null) {
			try {
				if (!mConnection.isClosed())
						mConnection.close();
			} catch (SQLException pE) { /* We don't need to know if this has happened. */ }
			
			mConnection = null;
		}
		// Make the manipulators useless, and free their resources
		if (mManipulators != null)
			for (TableManipulator m : mManipulators)
				m.close();
		
		mManipulators = null;
		mTables = null;
		// Add the registered resources to the Hibernate configuration
		for (String resource : mResources)
			mConfiguration.addResource(resource);

		mResources = null;
		// Build the factory
		mFactory = mConfiguration.buildSessionFactory();

		mSession = mFactory.openSession();
	}
	/**
	 * Deletes the supplied object.
	 */
	public void delete(Object pObject) {
		mSession.beginTransaction();
		
		mSession.delete(pObject);
		
		mSession.getTransaction().commit();
	}
	private int mErrors = 0;
	/**
	 * Saves or updates the supplied object.
	 * 
	 * If this method fails the preset (by the database_restart_threshold argument) amount of times, it will attempt to restart the database and Hibernate.
	 */
	public void save(Object pObject) {
		try {
			mSession.beginTransaction();
			
			mSession.saveOrUpdate(pObject);
			
			mSession.getTransaction().commit();
			
			mErrors = 0;
		} catch (Exception e) {
			mErrors++;
			
			logger.warning(tp.get("storage.databaseError") + ": ", e);
			
			if (mErrors == RESTART_THRESHOLD) {
				restart();
				
				mErrors = 0;
				
				save(pObject);
			}
		} finally {
//			mSession.flush();
		}
	}
	/**
	 * Returns a Hibernate Session.  This is only meant for advanced database usage.  For basic usage, use save(), delete(), and list().
	 * @param pNewSession If <i>true</i>, a new dedicated Session will be created.  If <i>false</i>, the internal one will be used.
	 * @return A Session, new or otherwise depends on the parameter.
	 */
	public Session getSession(boolean pNewSession) {
		if (pNewSession)
			return mFactory.openSession();
		else
			return mSession;
	}
	
	/**
	 * Returns a list of objects.
	 * @param <T> The type of objects the query should look for (and the returned List contain).
	 * @param pClass The class of objects the returned List should contain.
	 * @param pArguments The arguments to send with the query (like where clauses and order).  Must be in HQL.
	 * @return a list of objects.
	 */
	@SuppressWarnings("unchecked")
	public <T> List<T> getList(Class<T> pClass, String pArguments) {
		Query q = mSession.createQuery(String.format("from %s %s", pClass.getSimpleName(), pArguments));
		
		List<?> list = q.list();
		
		return (List<T>)list;
	}
	/**
	 * Shuts down Hibernate and the database.
	 */
	public void close() {
		logger.info("Shutting Down Hibernate");
		mSession.flush();

		mFactory.close();
		
		mFactory = null;
		
		try {
			logger.info("Shutting Down Database");
			DriverManager.getConnection(mConfiguration.getProperty("connection.url")).createStatement().execute("shutdown");
		} catch (SQLException e) {
			logger.warning(tp.get("storage.shutDownError"));
		}
	}
	/**
	 * Restarts Hibernate and the database.  Calls close() then start().
	 */
	public void restart() {
		close();
		
		start();
	}
	
	public static void main(String[] pArgs) {
		Database db = new Database();
		
		DatabaseTableManipulator tm = db.getTableManipulator();
		
		tm.addScript("mbsl/commons/mbsl_commons.sql");
	}
	
	private class TableManipulator implements DatabaseTableManipulator {
		private TableManipulator() {
			mCommands = new ArrayList<String>();
		}
		
		@Override
		public void add(String pCommand) {
			errorIfStarted("storage.manipulateBeforeStart");
			
			mCommands.add(pCommand);
		}
		
		@Override
		public void addScript(String pResourceName) {
			BufferedReader reader = new BufferedReader(new InputStreamReader(ClassLoader.getSystemResourceAsStream(pResourceName)));
			
			try {
				while (reader.ready()) {
					String line = reader.readLine().trim();
					
					if (!line.isEmpty() && !line.startsWith("--"))
						add(line);
				}
			} catch (IOException pE) {
				System.out.println("Terror error"); //TODO
			}
		}

		@Override
		public boolean exists(String pTableName) {
			errorIfStarted("storage.manipulateBeforeStart");
			
			if (mTables == null) {
				try {
					DatabaseMetaData meta = mConnection.getMetaData();
					mTables = new ArrayList<String>();
					
					ResultSet rs = meta.getTables(null, null, null, null);
					
					while (rs.next()) {
						String name = rs.getString(3); // 3 is the column with the table names in it.
						
						if (!name.startsWith("SYSTEM_"))
							mTables.add(name.toLowerCase());
					}
				} catch (SQLException pE) {
					throw new RuntimeException(pE);
				}
			}
			
			pTableName = pTableName.toLowerCase();
			
			return mTables.contains(pTableName);
		}

		@Override
		public void commit() {
			errorIfStarted("storage.manipulateBeforeStart");
			
			if (mConnection == null)
				throw new IllegalStateException(tp.get("storage.noConnection"));

			try {
				Statement statement = mConnection.createStatement();
				
				for (String command : mCommands)
					statement.addBatch(command);
				
				statement.executeBatch();
				
				mCommands.clear();
				// Closing happens up in start()
			} catch (SQLException pE) {
				throw new RuntimeException(pE);
			}
		}
		private void close() {
			mCommands = null;
		}
		
		private List<String> mCommands;
	}
	
	/** Checks if the Database is started, and throws an IllegalStateException with the passed message if it is */
	private void errorIfStarted(String pMessageKey) {
		if (mFactory != null)
			throw new IllegalStateException(tp.get(pMessageKey));
	}
	
	/** The list of tables in the database */
	private List<String> mTables;
	/** The Connection used by the table manipulators */
	private Connection mConnection;
	/** Our own logger */
	private static Logger logger = new Logger(Database.class);
	/** Our own TextProvider */
	private static TextProvider tp = Core.tp;
	/** The list of created manipulators */
	private List<TableManipulator> mManipulators;
	
	private SessionFactory mFactory;
	private Session mSession;
	private Configuration mConfiguration;
	/** The list of resources to be added to Hibernate before it starts */
	private List<String> mResources;
	
	/** The number of times to restart before giving up */
	public static int RESTART_THRESHOLD = 2;
}
